package okl.test.motionChart
{
	import com.fizzback.utils.ColorsHelper;
	import com.fizzback.utils.DateHelper;
	
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.geom.Point;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	import flash.utils.setTimeout;
	
	import flashx.textLayout.events.UpdateCompleteEvent;
	import flashx.textLayout.formats.Category;
	
	import model.BubbleData;
	
	import mx.charts.AxisRenderer;
	import mx.charts.BubbleChart;
	import mx.charts.ChartItem;
	import mx.charts.DateTimeAxis;
	import mx.charts.HitData;
	import mx.charts.LinearAxis;
	import mx.charts.LogAxis;
	import mx.charts.chartClasses.CartesianDataCanvas;
	import mx.charts.chartClasses.ChartElement;
	import mx.charts.chartClasses.IAxis;
	import mx.charts.chartClasses.IAxisRenderer;
	import mx.charts.chartClasses.NumericAxis;
	import mx.charts.chartClasses.PolarDataCanvas;
	import mx.charts.chartClasses.Series;
	import mx.charts.effects.SeriesInterpolate;
	import mx.charts.renderers.CircleItemRenderer;
	import mx.charts.renderers.LineRenderer;
	import mx.charts.series.BubbleSeries;
	import mx.charts.series.LineSeries;
	import mx.charts.series.items.BubbleSeriesItem;
	import mx.collections.ArrayCollection;
	import mx.collections.Sort;
	import mx.collections.SortField;
	import mx.containers.Box;
	import mx.containers.Canvas;
	import mx.controls.Alert;
	import mx.core.ClassFactory;
	import mx.core.UIComponent;
	import mx.effects.Resize;
	import mx.events.DynamicEvent;
	import mx.events.FlexEvent;
	import mx.events.ResizeEvent;
	import mx.formatters.DateFormatter;
	import mx.graphics.GradientBase;
	import mx.graphics.GradientEntry;
	import mx.graphics.IStroke;
	import mx.graphics.LinearGradient;
	import mx.graphics.LinearGradientStroke;
	import mx.graphics.SolidColor;
	import mx.graphics.SolidColorStroke;
	import mx.graphics.Stroke;
	import mx.utils.StringUtil;
	import mx.utils.UIDUtil;
	
	import okl.test.motionChart.interfaces.IMotionBubbleData;
	import okl.test.motionChart.parts.ChartLineMask;
	import okl.test.motionChart.renderers.CheckBoxListItemRenderer;
	import okl.test.motionChart.renderers.MotionBubbleDataTip;
	import okl.test.motionChart.renderers.MotionBubbleItemRenderer;
	import okl.test.motionChart.renderers.SimpleLinearAxisRenderer;
	import okl.test.motionChart.renderers.TimePlayAxisRenderer;
	import okl.test.motionChart.renderers._CustomRenderer;
	
	import org.hamcrest.DiagnosingMatcher;
	
	import spark.components.List;
	import spark.effects.interpolation.RGBInterpolator;
	
	/**
	 * Monster that governs chart 
	 *  - initialize and binds together main components
	 *  - setup (axises)
	 *  - bindings with size/color comboboxes.
	 *  - preprocess data (categories, visible series, interpolation for lines)
	 * 
	 * NOTE:
	 * It is kind of presenattion controller class.
	 * This logic can be moved to MotionChart component (4.5 skin model)
	 * 
	 */
	public class BubbleChartDescriptor extends EventDispatcher
	{
		//CONST
		private static var chartDataGroupProperty:String="category";
		private static var nullGroupName:String="<NULL>";
		
		//main components
		private var chart:BubbleChart;
		private var liCategories:List;
		
		//chart axises
		private var hDateAxis:DateTimeAxis = new DateTimeAxis();
		private var timeAxisRenderer:TimePlayAxisRenderer;
		private var hLinearAxis:LinearAxis = new LinearAxis();	
		private var hLogAxis:LogAxis = new LogAxis();
		private var vLinearAxis:LinearAxis = new LinearAxis();
		private var vLogAxis:LogAxis = new LogAxis();		
		private var hCurAxis:IAxis;
		private var vCurAxis:IAxis;
		//for date axis hLinear is false
		[Bindable]public var hLinear:Boolean;
		
		//Data state		
		private var _chartData:ArrayCollection;
		private var _chartDataGroup:ArrayCollection
		private var allSeries:Array, allBoxes:Array;		
	
		private var bkgCanvas:ChartElement;
				
		public function BubbleChartDescriptor(chart:BubbleChart, liCats:List)
		{
			this.chart = chart;
			this.chart.setStyle("minRadius", 5);
			this.chart.setStyle("maxRadius", 20);
			this.liCategories = liCats;
			this.liCategories.addEventListener(CheckBoxListItemRenderer.CHECK, onListCheck);
		}
		
		//{DATA_PREPROCESS
		public function get chartData():ArrayCollection{return _chartData;}
		public function set chartData(value:ArrayCollection):void{
			if(value==_chartData)return;
			_chartData=value;
			//update runtime props
			_items = new Dictionary();
			_itemDescriptors = new Dictionary();
			
			var groupDict:Dictionary = new Dictionary();
			for each(var data:IMotionBubbleData in chartData){
				addItem(data);
				//grouping by cat
				var propValue:String = data[chartDataGroupProperty] || nullGroupName;
				var groupDictValue:Object = groupDict[propValue];
				if(groupDictValue==null){
					groupDictValue  = {label: propValue, children: new Array()};
					groupDict[propValue]=groupDictValue;
				}
				groupDictValue.children.push(data);
			}
			
			_chartDataGroup = new ArrayCollection();
			allSeries = new Array();
			allBoxes = new Array();
			
			var chartLineMask:ChartLineMask;
			bkgCanvas = new ChartElement();
			for each(var mapData:Object in groupDict){
				_chartDataGroup.addItem(mapData);
				
				var ls:LineSeries = new LineSeries();
				ls.xField="date";			
				ls.yField="score";
				ls.radius = 5;
				ls.dataProvider=mapData.children;	
				allSeries.push(ls);				
				
				chartLineMask = new ChartLineMask();
				chartLineMask.data=mapData.label;
				chartLineMask.gradient = this.gcolors;				
				chartLineMask.x=0;//chart.getStyle("gutterLeft");
				chartLineMask.width= chart.width-chartLineMask.x;
				chartLineMask.height=chart.height-chart.getStyle("gutterBottom");				
				chartLineMask.mask =ls;
				chartLineMask.cacheAsBitmap=ls.cacheAsBitmap=true;
				bkgCanvas.addChild(chartLineMask);
				
				allBoxes.push(chartLineMask);
				
			}
			
			bkgCanvas.width = chart.width;
			bkgCanvas.height = chart.height;
			chart.backgroundElements.push(bkgCanvas);
			
			var rearrangeData:SeriesInterpolate = new SeriesInterpolate();
			rearrangeData.duration = 1000;
			rearrangeData.minimumElementDuration =0;
			rearrangeData.elementOffset = 0;
			var bSer:BubbleSeries = new BubbleSeries();
			bSer.setStyle("showDataEffect", rearrangeData);			
			allSeries.push(bSer);
			
			chart.series = allSeries;
			setupChartRenderers();
			
			
			var labelSort:Sort = new Sort();
			labelSort.fields = [new SortField("label")];
			_chartDataGroup.sort = labelSort;
			_chartDataGroup.refresh();
			this.liCategories.dataProvider = _chartDataGroup;
			_invalidateTimeAxis();
			
			//set date to next day, to show bubbles
			currenDate=new Date();
			setTimeout(function():void{currenDate=new Date(minDate.time + _hAxisDateInterval);}, 100);
			
		}
		//}endregion
		

		//{AXIS_BUBBLE_RENDERERS
		public function setupChartRenderers():void{		
			hCurAxis=hDateAxis;
			vCurAxis=vLinearAxis;
			
			var hAxisRenderer:IAxisRenderer = new SimpleLinearAxisRenderer();
			var rAxis:UIComponent = chart.horizontalAxisRenderer as UIComponent;
			hDateAxis.labelFunction = funcDateFormat;
			hAxisRenderer.axis = hCurAxis; //!			
			
			timeAxisRenderer = new TimePlayAxisRenderer();
			timeAxisRenderer.addEventListener(TimePlayAxisRenderer.EVT_DATE_CHANGE, onDateChange);
			timeAxisRenderer.axis = hDateAxis;
			hAxisRenderer.placement=timeAxisRenderer.placement="bottom";
			
			initAxisRenderer(chart.horizontalAxisRenderer as UIComponent);			
			initAxisRenderer(chart.verticalAxisRenderer as UIComponent);
			
			chart.horizontalAxisRenderers=[hAxisRenderer, timeAxisRenderer];
			chart.horizontalAxis = hCurAxis;
			chart.verticalAxis = vCurAxis;
			
			var tipRenderer:ClassFactory = new ClassFactory(MotionBubbleDataTip);
			tipRenderer.properties = {chartDescriptor: this};
			chart.setStyle("dataTipRenderer", tipRenderer);
			chart.dataTipFunction = function(hitData:HitData):String{return hitData.item.category;}
						
			var iRenderer:ClassFactory = new ClassFactory(MotionBubbleItemRenderer);
			iRenderer.properties = {chartDescriptor: this};
			targetSeries.setStyle("itemRenderer", iRenderer);
			chart.addEventListener(ResizeEvent.RESIZE, onChartResizeComplete);
			_invalidateTimeAxis();			
		}
		
		private  function initAxisRenderer(rAxis:UIComponent):void{
			var chartStroke:IStroke = new SolidColorStroke(0xB2B2B2, 1);
			rAxis.setStyle("tickLength", 2);
			
			rAxis.setStyle("tickPlacement", "outside");
			rAxis.setStyle("axisStroke", chartStroke);
			
			rAxis.setStyle("minorTickLength", 4);
			rAxis.setStyle("minorTickStroke", chartStroke);
			rAxis.setStyle("minorTickPlacement", "outside");
			
			rAxis.setStyle("tickStroke", chartStroke);
			
			rAxis.setStyle("showLine", true);
			rAxis.setStyle("showLabels", true);
		}
		
		
		private var df:DateFormatter= new DateFormatter();
		private function funcDateFormat(value:Number, prev:Number, target:Object):String{
			df.formatString="DD/MM/YYYY"
			return df.format(new Date(value));
		}
				

		
		private function onDateChange(event:DynamicEvent):void{
			currenDate = event.date as Date;
		}
		
		private var _currenDate:Date;		
		public function set currenDate(value:Date):void{
			if(_currenDate==value)return;
			_currenDate=value;
			
			updateLines();		
			updateDisplayData();			
			timeAxisRenderer.selectedDate=_currenDate;
						
		}
		
		private function updateLines():void{
			
			//NEW
			var temp:Number =(_currenDate.time-minDate.time)/(maxDate.time-minDate.time)
			var mw:Number = temp * (chart.width - chart.getStyle("gutterLeft"));
			
			var test:Point =(chart.series[0] as LineSeries).dataToLocal(_currenDate.time, 0);
			mw = test.x - 1;
			for(var i:uint=0; i<allBoxes.length; i++){
				var show:Boolean = (TEMP_VIS_GROUP.getItemIndex( allBoxes[i].data )!=-1);
				var series:LineSeries = chart.series[i] as LineSeries;
				series.interactive=false;
				
				var curBox:DisplayObject = allBoxes[i] as DisplayObject;
				curBox.width= (show) ? mw : 0;
				curBox.height=chart.height-chart.getStyle("gutterBottom");
			}
			
		}
		
		
		//{region AXIS
		//BubbleData
		private static var TMP_PROP_MAP:Object={
			date: Date
		}
		
		
		public function set vProp(value:String):void{
			if(targetSeries.yField==value)return;
			targetSeries.yField=value;
			
			invalidateAxisExtremes();
		}
		public function set hProp(value:String):void{
			if(targetSeries.xField==value)return;
			targetSeries.xField=value;
			if(TMP_PROP_MAP[value]==Date){			
				hCurAxis = hDateAxis;
				_invalidateTimeAxis();
			}else{
				hCurAxis = hLinearAxis;
			}
			chart.horizontalAxis = hCurAxis;
			
			invalidateAxisExtremes();
			
			hLinear = (hCurAxis!=this.hDateAxis);
			
			
		}
		
		public function set vAxisType(value:Class):void{
		}
		
		public function set hAxisType(value:Class):void{
		}
		
		public function set rProp(value:String):void{
			if(targetSeries.radiusField==value)return;
			targetSeries.radiusField=value || "__radius";
			
			invalidateAxisExtremes();
		}
		
		
		private function invalidateAxisExtremes():void{
			var targetAxis:LinearAxis;
			if(targetSeries.xField && chart.horizontalAxis is LinearAxis){
				targetAxis = chart.horizontalAxis as LinearAxis;
				targetAxis.minimum=minProps[targetSeries.xField];
				targetAxis.maximum=maxProps[targetSeries.xField];
			}
			if(targetSeries.yField && chart.verticalAxis is LinearAxis){
				targetAxis = chart.verticalAxis as LinearAxis;
				targetAxis.minimum=minProps[targetSeries.yField]-1;
				targetAxis.maximum=maxProps[targetSeries.yField]+1;
			}
			
		}
		
		
		private function _invalidateTimeAxis():void{
			invalidateDtaeAxis(hCurAxis);
			invalidateDtaeAxis(hDateAxis);
			dispatchEvent(new Event("chartDataChanged"));
		}
		
		private function invalidateDtaeAxis(axis:IAxis):void{
			var targetAxis:DateTimeAxis = axis as DateTimeAxis;
			if(targetAxis==null || minDate==null)return;
			targetAxis.minimum=minDate;
			targetAxis.maximum=maxDate;			
			targetAxis.dataUnits=targetAxis.labelUnits=_hAxisDateUnits;	
			
		}
		
		private var _hAxisDateUnits:String = "weeks";
		private var _hAxisDateInterval:Number = DateHelper.MS_PER_DAY;
		public function set hAxisDateUnits(value:String):void{
			_hAxisDateUnits=value;			
			_invalidateTimeAxis();			
		}

		private function get targetSeries():BubbleSeries{return (chart.series[chart.series.length-1] as BubbleSeries);}
		//}endregion
		

		
		
		

	
		
		private var lineMask:DisplayObject;
		public var TEMP_VIS_GROUP:ArrayCollection=new ArrayCollection();
		
		//{region FILTERING
		[Bindable]public var minDate:Date;
		[Bindable]public var maxDate:Date;
		private function updateDisplayData():void{
			//need to interpolate here
			var result:ArrayCollection = new ArrayCollection();
			for each(var item:Object in _chartDataGroup){				
				var bubblesAround:Array = _findNearestPoints(item.children, _currenDate);
				if(bubblesAround==null)continue;
				var b:IMotionBubbleData = createFakeBubble(_currenDate, bubblesAround);
				result.addItem(b);
				
			}
			
			chart.dataProvider=result;
			
			//invalidate after data invalidation (in next invalid. pass)
			chart.callLater(invalidateChart);
			
		}
		
		private function onChartResizeComplete(skipEvent:Event=null):void{
			chart.callLater(invalidateChart);
		}
		
		private function invalidateChart(skipEvent:Event=null):void{
			updateDataTips();
			updateLines();
		}
		
		//find 2 dates
		
		private function createFakeBubble(curDate:Date, realBubbles:Array):IMotionBubbleData{
			var prev:IMotionBubbleData=realBubbles[0] as IMotionBubbleData;
			var next:IMotionBubbleData=realBubbles[1] as IMotionBubbleData;
			if(prev==next)return prev;
			//TODO: Remove dependency on  BubbleData
			var fake:IMotionBubbleData = new BubbleData();
			fake.category = prev.category;
			fake.date=curDate;
			var dateChange:Number = (curDate.time-prev.date.time) / (next.date.time-prev.date.time);
			for(var i:uint=0; i<_montorProps.length; i++){
				var prop:String = _montorProps[i];
				var val1:Number = Number(prev[prop]);
				var val2:Number = Number(next[prop]);
				var val:Number= Number(dateChange*(val2-val1) + val1);
				fake[prop]=val;
			};
			return fake;
		}
		
		
		//assumption - dates are in sequential order and sorted
		private function _findNearestPoints(targetArr:Array, targetDate:Date):Array{
			var prev:IMotionBubbleData, next:IMotionBubbleData;
			for(var i:uint=0; i<targetArr.length; i++){
				var testData:IMotionBubbleData = targetArr[i] as IMotionBubbleData;				
				if(testData.date.time>=targetDate.time){
					next=testData;
					if(prev==null){
						if(next.date==minDate){
							return [prev, next];
						}else{
							return null; //has no data for current category on start
						}				
					}				
					return [prev, next];
				}
				prev=testData;
			}		
			return null; //has no data for current category at the end
			
		}		
		//}endregion
		
		
		//{region RUNTIME_PROPS_SETTING

		private var _items:Dictionary;/*<BubbleData>*/
		private var _itemDescriptors:Dictionary;
		public function setItemDescriptor(key:Object, propName:String, propValue:Object):void{
			var targetDescriptor:Object = _itemDescriptors[key];
			if(targetDescriptor==null){
				targetDescriptor=new Object();
				_itemDescriptors[key]=targetDescriptor;
			}
			targetDescriptor[propName]=propValue;
		}
		
		public function getItemDescriptorValue(key:Object, propName:String):Object{
			var targetDescriptor:Object = _itemDescriptors[key];
			if(targetDescriptor==null)return null;
			return targetDescriptor[propName];
		}
		
		
		private var _montorProps:Array = ["volume", "score"];
		private var maxProps:Dictionary=new Dictionary();
		private var minProps:Dictionary=new Dictionary();
		private function addItem(key:Object):void{
			_items[key]=key;
			
			for each(var propName:String in _montorProps){
				var objValue:Object = key[propName];
				maxProps[propName] = maxProps[propName] ||objValue;
				minProps[propName] = minProps[propName] || objValue;
				if(objValue>maxProps[propName])maxProps[propName]=objValue;
				if(objValue<minProps[propName])minProps[propName]=objValue;
			}
			
			var targetDate:Date = key.date;
			minDate=minDate || new Date(targetDate.time);
			maxDate=maxDate ||  new Date(targetDate.time);
			if(targetDate.time<minDate.time)minDate=new Date(targetDate.time);
			if(targetDate.time>maxDate.time)maxDate=new Date(targetDate.time);	
		}
		//}endregion
		
				
		//{region COLORS fotr bubbles
		
		public static const BUBBLE_COLOR_UNIQUE:String="BUBBLE_COLOR_UNIQUE";
		public static const BUBBLE_COLOR_SAME:String="BUBBLE_COLOR_SAME";
		
		//gradient from RED(top) to BLUE(bottom). When we choose target property in Colour combobox 
		public var gcolors:GradientBase= $tempDefaultGradient();
		private static var gradientEntries:Array= [
			new GradientEntry(0xCC0000, 0), /*need to start with 0*/
			new GradientEntry(0xCC0000, 0.16),
			new GradientEntry(0xCCFF00, 0.5),
			new GradientEntry(0x0099CC, 0.7),
			new GradientEntry(0x0033CC, 0.97)
		];
		
		private static function $tempDefaultGradient():GradientBase{
			var result:LinearGradient = new LinearGradient();
			result.entries = gradientEntries;
			return result;		
		}		
		private var _bubbleColorRules:String;
		private var _defaultSameBubbleColor:uint;
		public function set bubbleColorRules(value:String):void{
			_bubbleColorRules=value;
			_defaultSameBubbleColor=ColorsHelper.getRandomColor();
			chart.dataProvider.refresh();
		}
		
		public function getBubbleColor(item:IMotionBubbleData):Number{
			var color:Number = 0;
			if(BUBBLE_COLOR_UNIQUE==_bubbleColorRules){
				color = Number(this.getItemDescriptorValue(item.category, "rndColor"));
				if(color==0){
					color=ColorsHelper.getRandomColor();
					this.setItemDescriptor(item.category, "rndColor", color);
				}
			}else if(BUBBLE_COLOR_SAME==_bubbleColorRules){
				color=_defaultSameBubbleColor;
			}else{
				var propName:String = _bubbleColorRules;
				var min:Number = Number(minProps[propName]);
				var max:Number = Number(maxProps[propName]);
				
				var chartMin:Number = 0;
				var chartMax:Number = max+min;
				
				var cur:Number = Number(item[propName]);
				var fraction:Number = cur/(chartMax-chartMin);
				
				color = ColorsHelper.getGradientFractionColor(fraction, gcolors);
				
			}
			return color;
		}
		
		//}endregion
		
		//{CATEGORIES_LIST and DATA_TIPS
		private function onListCheck(event:DynamicEvent):void{
			if(event.selected){
				TEMP_VIS_GROUP.addItem(event.categoryName);
			}else{
				TEMP_VIS_GROUP.removeItemAt(TEMP_VIS_GROUP.getItemIndex(event.categoryName));
			}
			
			updateLines();			
			updateDataTips();
			
		}
		
		private function updateDataTips():void{
			//update series
			var dataTips:Array = new Array();
			for each(var sItem:BubbleSeriesItem in targetSeries.items){
				var dataItem:IMotionBubbleData = sItem.item as IMotionBubbleData;
				if(TEMP_VIS_GROUP.getItemIndex(dataItem.category) != -1){
					dataTips.push(sItem);
				}
			}
			targetSeries.dataTipItems = dataTips;	
		}
		//}endregion
		
		
		//{region LOG_AXIS_SCALE
		public static const VSCALE_LIN:String = "linear"; /*default*/
		public static const VSCALE_LOG:String = "log";
		
		private var _vScale:String=VSCALE_LIN;
		[Bindable("vScaleChanged")]public function set vScale(value:String):void{
			if(value==vScale)return;
			_vScale=value;
			chart.verticalAxis = (vScale==VSCALE_LOG) ? vLogAxis : vLinearAxis;
			dispatchEvent(new Event("vScaleChanged"));
		}
		public function get vScale():String{return _vScale;}
		
		private var _hScale:String=VSCALE_LIN;
		[Bindable("vScaleChanged")]public function set hScale(value:String):void{
			if(value==hScale)return;
			_hScale=value;
			hCurAxis = (hScale==VSCALE_LOG) ? hLogAxis : hLinearAxis;		
			chart.horizontalAxis = hCurAxis;			
			invalidateAxisExtremes()
			
			dispatchEvent(new Event("hScaleChanged"));
		}
		public function get hScale():String{return _hScale;}
		//}endregion
		
	}
}